/*
 * Hibernate, Relational Persistence for Idiomatic Java
 *
 * License: GNU Lesser General Public License (LGPL), version 2.1 or later.
 * See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
 */
package org.hibernate.hql.internal.ast;

import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.StringReader;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.hibernate.QueryException;
import org.hibernate.hql.internal.antlr.HqlBaseParser;
import org.hibernate.hql.internal.antlr.HqlTokenTypes;
import org.hibernate.hql.internal.ast.util.ASTUtil;
import org.hibernate.hql.internal.ast.util.TokenPrinters;
import org.hibernate.internal.CoreLogging;
import org.hibernate.internal.CoreMessageLogger;
import org.hibernate.internal.util.StringHelper;

import antlr.ASTPair;
import antlr.MismatchedTokenException;
import antlr.RecognitionException;
import antlr.Token;
import antlr.TokenStreamException;
import antlr.collections.AST;

/**
 * Implements the semantic action methods defined in the HQL base parser to keep the grammar
 * source file a little cleaner.  Extends the parser class generated by ANTLR.
 *
 * @author Joshua Davis (pgmjsd@sourceforge.net)
 */
public final class HqlParser extends HqlBaseParser {
	private static final CoreMessageLogger LOG = CoreLogging.messageLogger( HqlParser.class );

	private final ParseErrorHandler parseErrorHandler;

	/**
	 * Get a HqlParser instance for the given HQL string.
	 *
	 * @param hql The HQL query string
	 *
	 * @return The parser.
	 */
	public static HqlParser getInstance(String hql) {
		return new HqlParser( hql );
	}

	private HqlParser(String hql) {
		// The fix for HHH-558...
		super( new HqlLexer( new StringReader( hql ) ) );
		parseErrorHandler = new ErrorTracker( hql );
		// Create nodes that track line and column number.
		setASTFactory( new HqlASTFactory() );
	}


	// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
	// trace logging hooks

	private int traceDepth;

	@Override
	public void traceIn(String ruleName) {
		if ( !LOG.isTraceEnabled() ) {
			return;
		}
		if ( inputState.guessing > 0 ) {
			return;
		}
		String prefix = StringHelper.repeat( '-', ( traceDepth++ * 2 ) ) + "-> ";
		LOG.trace( prefix + ruleName );
	}

	@Override
	public void traceOut(String ruleName) {
		if ( !LOG.isTraceEnabled() ) {
			return;
		}
		if ( inputState.guessing > 0 ) {
			return;
		}
		String prefix = "<-" + StringHelper.repeat( '-', ( --traceDepth * 2 ) ) + " ";
		LOG.trace( prefix + ruleName );
	}

	// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
	// error handling hooks

	@Override
	public void reportError(RecognitionException e) {
		parseErrorHandler.reportError( e ); // Use the delegate.
	}

	@Override
	public void reportError(String s) {
		parseErrorHandler.reportError( s ); // Use the delegate.
	}

	@Override
	public void reportWarning(String s) {
		parseErrorHandler.reportWarning( s );
	}

	public ParseErrorHandler getParseErrorHandler() {
		return parseErrorHandler;
	}


	// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
	// Semantic actions

	/**
	 * Overrides the base behavior to retry keywords as identifiers.
	 *
	 * @param token The token.
	 * @param ex The recognition exception.
	 *
	 * @return The new AST.
	 *
	 * @throws antlr.RecognitionException if the substitution was not possible.
	 * @throws antlr.TokenStreamException if the substitution was not possible.
	 */
	@Override
	public AST handleIdentifierError(Token token, RecognitionException ex)
			throws RecognitionException, TokenStreamException {
		// If the token can tell us if it could be an identifier...
		if ( token instanceof HqlToken ) {
			HqlToken hqlToken = (HqlToken) token;
			// ... and the token could be an identifier and the error is
			// a mismatched token error ...
			if ( hqlToken.isPossibleID() && ( ex instanceof MismatchedTokenException ) ) {
				MismatchedTokenException mte = (MismatchedTokenException) ex;
				// ... and the expected token type was an identifier, then:
				if ( mte.expecting == HqlTokenTypes.IDENT ) {
					// Use the token as an identifier.
					reportWarning(
							"Keyword  '"
									+ token.getText()
									+ "' is being interpreted as an identifier due to: " + mte.getMessage()
					);
					// Add the token to the AST.
					ASTPair currentAST = new ASTPair();
					token.setType( HqlTokenTypes.WEIRD_IDENT );
					astFactory.addASTChild( currentAST, astFactory.create( token ) );
					consume();
					return currentAST.root;
				}
			} // if
		} // if
		// Otherwise, handle the error normally.
		return super.handleIdentifierError( token, ex );
	}

	/**
	 * Returns an equivalent tree for (NOT (a relop b) ), for example:<pre>
	 * (NOT (GT a b) ) => (LE a b)
	 * </pre>
	 *
	 * @param x The sub tree to transform, the parent is assumed to be NOT.
	 *
	 * @return The equivalent sub-tree.
	 */
	@Override
	public AST negateNode(AST x) {
		//TODO: switch statements are always evil! We already had bugs because
		//      of forgotten token types. Use polymorphism for this!
		switch ( x.getType() ) {
			case OR: {
				x.setType( AND );
				x.setText( "{and}" );
				x.setFirstChild( negateNode( x.getFirstChild() ) );
				x.getFirstChild().setNextSibling( negateNode( x.getFirstChild().getNextSibling() ) );
				return x;
			}
			case AND: {
				x.setType( OR );
				x.setText( "{or}" );
				x.setFirstChild( negateNode( x.getFirstChild() ) );
				x.getFirstChild().setNextSibling( negateNode( x.getFirstChild().getNextSibling() ) );
				return x;
			}
			case EQ: {
				// (NOT (EQ a b) ) => (NE a b)
				x.setType( NE );
				x.setText( "{not}" + x.getText() );
				return x;
			}
			case NE: {
				// (NOT (NE a b) ) => (EQ a b)
				x.setType( EQ );
				x.setText( "{not}" + x.getText() );
				return x;
			}
			case GT: {
				// (NOT (GT a b) ) => (LE a b)
				x.setType( LE );
				x.setText( "{not}" + x.getText() );
				return x;
			}
			case LT: {
				// (NOT (LT a b) ) => (GE a b)
				x.setType( GE );
				x.setText( "{not}" + x.getText() );
				return x;
			}
			case GE: {
				// (NOT (GE a b) ) => (LT a b)
				x.setType( LT );
				x.setText( "{not}" + x.getText() );
				return x;
			}
			case LE: {
				// (NOT (LE a b) ) => (GT a b)
				x.setType( GT );
				x.setText( "{not}" + x.getText() );
				return x;
			}
			case LIKE: {
				// (NOT (LIKE a b) ) => (NOT_LIKE a b)
				x.setType( NOT_LIKE );
				x.setText( "{not}" + x.getText() );
				return x;
			}
			case NOT_LIKE: {
				// (NOT (NOT_LIKE a b) ) => (LIKE a b)
				x.setType( LIKE );
				x.setText( "{not}" + x.getText() );
				return x;
			}
			case IN: {
				x.setType( NOT_IN );
				x.setText( "{not}" + x.getText() );
				return x;
			}
			case NOT_IN: {
				x.setType( IN );
				x.setText( "{not}" + x.getText() );
				return x;
			}
			case IS_NULL: {
				// (NOT (IS_NULL a b) ) => (IS_NOT_NULL a b)
				x.setType( IS_NOT_NULL );
				x.setText( "{not}" + x.getText() );
				return x;
			}
			case IS_NOT_NULL: {
				// (NOT (IS_NOT_NULL a b) ) => (IS_NULL a b)
				x.setType( IS_NULL );
				x.setText( "{not}" + x.getText() );
				return x;
			}
			case BETWEEN: {
				// (NOT (BETWEEN a b) ) => (NOT_BETWEEN a b)
				x.setType( NOT_BETWEEN );
				x.setText( "{not}" + x.getText() );
				return x;
			}
			case NOT_BETWEEN: {
				// (NOT (NOT_BETWEEN a b) ) => (BETWEEN a b)
				x.setType( BETWEEN );
				x.setText( "{not}" + x.getText() );
				return x;
			}
/* This can never happen because this rule will always eliminate the child NOT.
			case NOT: {
				// (NOT (NOT x) ) => (x)
				return x.getFirstChild();
			}
*/
			default: {
				// Just add a 'not' parent.
				AST not = super.negateNode( x );
				if ( not != x ) {
					// relink the next sibling to the new 'not' parent
					not.setNextSibling( x.getNextSibling() );
					x.setNextSibling( null );
				}
				return not;
			}
		}
	}

	/**
	 * Post process equality expressions, clean up the subtree.
	 *
	 * @param x The equality expression.
	 *
	 * @return The clean sub-tree.
	 */
	@Override
	public AST processEqualityExpression(AST x) {
		if ( x == null ) {
			LOG.processEqualityExpression();
			return null;
		}

		int type = x.getType();
		if ( type == EQ || type == NE ) {
			boolean negated = type == NE;
			if ( x.getNumberOfChildren() == 2 ) {
				AST a = x.getFirstChild();
				AST b = a.getNextSibling();
				// (EQ NULL b) => (IS_NULL b)
				if ( a.getType() == NULL && b.getType() != NULL ) {
					return createIsNullParent( b, negated );
				}
				// (EQ a NULL) => (IS_NULL a)
				else if ( b.getType() == NULL && a.getType() != NULL ) {
					return createIsNullParent( a, negated );
				}
				else if ( b.getType() == EMPTY ) {
					return processIsEmpty( a, negated );
				}
				else {
					return x;
				}
			}
			else {
				return x;
			}
		}
		else {
			return x;
		}
	}

	private AST createIsNullParent(AST node, boolean negated) {
		node.setNextSibling( null );
		int type = negated ? IS_NOT_NULL : IS_NULL;
		String text = negated ? "is not null" : "is null";
		return ASTUtil.createParent( astFactory, type, text, node );
	}

	private AST processIsEmpty(AST node, boolean negated) {
		node.setNextSibling( null );
		// NOTE: Because we're using ASTUtil.createParent(), the tree must be created from the bottom up.
		// IS EMPTY x => (EXISTS (QUERY (SELECT_FROM (FROM x) ) ) )
		AST ast = createSubquery( node );
		ast = ASTUtil.createParent( astFactory, EXISTS, "exists", ast );
		// Add NOT if it's negated.
		if ( !negated ) {
			ast = ASTUtil.createParent( astFactory, NOT, "not", ast );
		}
		return ast;
	}

	private AST createSubquery(AST node) {
		AST ast = ASTUtil.createParent( astFactory, RANGE, "RANGE", node );
		ast = ASTUtil.createParent( astFactory, FROM, "from", ast );

		AST alias = ASTUtil.createSibling( astFactory, ALIAS, "_", node );
		ASTUtil.insertChild( ASTUtil.createSibling( astFactory, SELECT, "select", ast ), astFactory.create(IDENT, alias.getText() ) );
		
		ast = ASTUtil.createParent( astFactory, SELECT_FROM, "SELECT_FROM", ast );
		ast = ASTUtil.createParent( astFactory, QUERY, "QUERY", ast );
		return ast;
	}

	public void showAst(AST ast, PrintStream out) {
		showAst( ast, new PrintWriter( out ) );
	}

	private void showAst(AST ast, PrintWriter pw) {
		TokenPrinters.HQL_TOKEN_PRINTER.showAst( ast, pw );
	}

	@Override
	public void matchOptionalFrom() throws RecognitionException, TokenStreamException {
		returnAST = null;
		ASTPair currentAST = new ASTPair();
		AST optionalFrom_AST = null;

		if ( LA( 1 ) == FROM ) {
			if ( LA( 2 ) != DOT ) {
				match( FROM );
				optionalFrom_AST = (AST) currentAST.root;
				returnAST = optionalFrom_AST;
			}
		}
	}

	@Override
	public void firstPathTokenWeakKeywords() throws TokenStreamException {
		int t = LA( 1 );
		switch ( t ){
			case DOT:
				LT(0).setType( IDENT );
		}
	}

	@Override
	public void handlePrimaryExpressionDotIdent() throws TokenStreamException {
		if ( LA( 2 ) == DOT && LA( 3 ) != IDENT ) {
			// See if the second lookahead token can be an identifier.
			HqlToken t = (HqlToken) LT( 3 );
			if ( t.isPossibleID() ) {
				// Set it!
				t.setType( IDENT );
				if ( LOG.isDebugEnabled() ) {
					LOG.debugf( "handleDotIdent() : new LT(3) token - %s", LT( 1 ) );
				}
			}
		}
	}

	@Override
	public void weakKeywords() throws TokenStreamException {

		int t = LA( 1 );
		switch ( t ) {
			case ORDER:
			case GROUP:
				// Case 1: Multi token keywords GROUP BY and ORDER BY
				// The next token ( LT(2) ) should be 'by'... otherwise, this is just an ident.
				if ( LA( 2 ) != LITERAL_by ) {
					LT( 1 ).setType( IDENT );
					if ( LOG.isDebugEnabled() ) {
						LOG.debugf( "weakKeywords() : new LT(1) token - %s", LT( 1 ) );
					}
				}
				break;
			default:
				// Case 2: The current token is after FROM and before '.'.
				if ( LA( 0 ) == FROM && t != IDENT && LA( 2 ) == DOT ) {
					HqlToken hqlToken = (HqlToken) LT( 1 );
					if ( hqlToken.isPossibleID() ) {
						hqlToken.setType( IDENT );
						if ( LOG.isDebugEnabled() ) {
							LOG.debugf( "weakKeywords() : new LT(1) token - %s", LT( 1 ) );
						}
					}
				}
				break;
		}
	}

	@Override
	public void expectNamedParameterName() throws TokenStreamException {
		// we expect the token following a COLON (':') to be the name of a named parameter.
		// if the following token is anything other than IDENT we convert its type if possible.

		// NOTE : the LT() call is more expensive than the LA() call; so we
		// use LA() first to see if LT() is needed.
		if ( LA( 1 ) != IDENT ) {
			final HqlToken nextToken = (HqlToken) LT( 1 );
			if ( nextToken.isPossibleID() ) {
				LOG.debugf(
						"Converting keyword [%s] following COLON to IDENT as an expected parameter name",
						nextToken.getText()
				);
				nextToken.setType( IDENT );
			}
		}
	}

	@Override
	public void handleDotIdent() throws TokenStreamException {
		// This handles HHH-354, where there is a strange property name in a where clause.
		// If the lookahead contains a DOT then something that isn't an IDENT...
		if ( LA( 1 ) == DOT && LA( 2 ) != IDENT ) {
			// See if the second lookahead token can be an identifier.
			HqlToken t = (HqlToken) LT( 2 );
			if ( t.isPossibleID() ) {
				// Set it!
				LT( 2 ).setType( IDENT );
				if ( LOG.isDebugEnabled() ) {
					LOG.debugf( "handleDotIdent() : new LT(2) token - %s", LT( 1 ) );
				}
			}
		}
	}

	@Override
	public void processMemberOf(Token n, AST p, ASTPair currentAST) {
		// convert MEMBER OF to the equivalent IN ELEMENTS structure...
		AST inNode = n == null ? astFactory.create( IN, "in" ) : astFactory.create( NOT_IN, "not in" );
		astFactory.makeASTRoot( currentAST, inNode );

		AST inListNode = astFactory.create( IN_LIST, "inList" );
		inNode.addChild( inListNode );
		AST elementsNode = astFactory.create( ELEMENTS, "elements" );
		inListNode.addChild( elementsNode );
		elementsNode.addChild( p );
	}

	private Map<String, Set<String>> treatMap;

	@Override
	protected void registerTreat(AST pathToTreat, AST treatAs) {
		final String path = toPathText( pathToTreat );
		final String subclassName = toPathText( treatAs );
		LOG.debugf( "Registering discovered request to treat(%s as %s)", path, subclassName );

		if ( treatMap == null ) {
			treatMap = new HashMap<>();
		}

		Set<String> subclassNames = treatMap.get( path );
		if ( subclassNames == null ) {
			subclassNames = new HashSet<>();
			treatMap.put( path, subclassNames );
		}
		subclassNames.add( subclassName );
	}

	private String toPathText(AST node) {
		final String text = node.getText();
		if ( text.equals( "." )
				&& node.getFirstChild() != null
				&& node.getFirstChild().getNextSibling() != null
				&& node.getFirstChild().getNextSibling().getNextSibling() == null ) {
			return toPathText( node.getFirstChild() ) + '.' + toPathText( node.getFirstChild().getNextSibling() );
		}
		return text;
	}

	public Map<String, Set<String>> getTreatMap() {
		return treatMap == null ? Collections.emptyMap() : treatMap;
	}

	public static void panic() {
		//overridden to avoid System.exit
		throw new QueryException( "Parser: panic" );
	}
}
